package br.mikhas.container.factory;

import static br.mikhas.container.factory.Scope.State.Idle;
import static br.mikhas.container.factory.Scope.State.Initializing;
import static br.mikhas.container.factory.Scope.State.Ready;

import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import br.mikhas.container.config.BeanDefinition;
import br.mikhas.container.config.DisposableBean;
import br.mikhas.container.factory.Scope.State;
import br.mikhas.reflector.Reflect;
import br.mikhas.util.Assert;

/**
 * Basic implementation of {@link BeanFactory} that imeplements bean creation
 * and wiring operations
 * 
 * @author Mikhail Domanoski
 * 
 */
public abstract class AbstractBeanFactory extends HierarchicalBeanFactory {

	private static final Logger log = LoggerFactory
			.getLogger(AbstractBeanFactory.class);

	private static final boolean debug = log.isDebugEnabled();

	/**
	 * The scopes registered on this factory
	 */
	private Map<String, Scope> registeredScopes = new HashMap<String, Scope>(3);

	/**
	 * The repository of resources definitions
	 */
	protected BeanLibrary library = new BeanLibrary();

	@Override
	public void registerScope(Scope scope) {
		Assert.notNull(scope);
		String name = scope.getName();

		if (debug)
			log
					.debug("Registering scope with alias \"" + name + "\": "
							+ scope);

		Scope sc = this.registeredScopes.get(name);

		if (sc != null) {
			log.warn("Overwriting scope registration");
			this.destroyScope(name);
		}

		this.registeredScopes.put(name, scope);
	}

	@Override
	public void initializeScope(final String scopeName) {
		Assert.notEmpty(scopeName);

		log.info("Initializing scope named \"" + scopeName + "\"");

		final Scope scope = getScope(scopeName);

		if (scope == null) {
			throw new ScopeInitializationException("Named scope not found: "
					+ scopeName);
		}

		State state = scope.getState();

		if (state != Idle) {
			throw new ScopeInitializationException("Cannot initialize scope "
					+ scopeName + " on state " + state);
		}

		scope.setState(Initializing);

		// Search on the resource library for the resources which belongs to the
		// named scope
		BeanDefinition[] scopedResources = this.library
				.getMatches(new BeanLibrary.ResourceScopeMatcher(scopeName));

		// Runs get thru all scope beans so it will force then to be initialized
		try {
			for (BeanDefinition def : scopedResources) {
				this.getResource(def.getName());
			}
		} catch (Exception e) {
			try {
				this.destroyScope(scopeName);
			} catch (Exception e1) {
			}
			throw new ScopeInitializationException(
					"Could not initialize scope: " + scopeName, e);
		}

		scope.setState(Ready);
	}

	/**
	 * Register a collection of resources
	 * 
	 * @param definitions
	 */
	protected void registerDefinitions(Collection<BeanDefinition> definitions) {
		Assert.notNull(definitions);

		for (BeanDefinition definition : definitions) {
			this.registerDefinition(definition);
		}
	}

	/**
	 * The definition to be registered on the library
	 */
	public void registerDefinition(BeanDefinition definition) {
		BeanDefinition res = definition;

		if (debug)
			log.debug("Registering resource definition into the factory: "
					+ definition);

		this.library.addResource(res);
	}

	/**
	 * Unregister a resource from the factory
	 */
	public void unregisterResource(BeanDefinition resource) {
		if (debug)
			log.debug("Unregistering resource definition from the factory: "
					+ resource);

		this.library.removeResource(resource);
		// TODO Unregister a resource
	}

	/**
	 * Checks on the default resource library if it contains a resource
	 * registered with the providen name.
	 */
	@Override
	public boolean containsResource(String name) {
		return this.library.containsResource(name);
	}

	protected Scope getScope(String name) {
		Scope scope = this.registeredScopes.get(name);
		if (scope == null)
			throw new BeanFactoryException("Scope not registered: " + name);
		return scope;
	}

	/**
	 * Internal operation to get or create the beans
	 */
	protected Object doGetResource(String name) {
		BeanDefinition resourceDef = this.library.getResource(name);

		// Could not find the resource
		if (resourceDef == null)
			return null;

		Scope resourceScope = getScope(resourceDef.getScope());

		State state = resourceScope.getState();

		if (state != Initializing && state != Ready)
			throw new BeanFactoryException(
					"The resource scope is no initialized: " + resourceScope);

		Object resource = resourceScope.getResource(name);

		// The resource has not been initialized
		if (resource == null && !resourceDef.isInitializingPhase()) {

			resource = this.initializeBean(resourceDef);

			// Adds the created resource
			resourceScope.addResource(name, resource);
		}

		return resource;
	}

	/**
	 * Initialize the resource and wires it to its dependencies.
	 * 
	 * 
	 * @param resourceDef
	 *            The resource definition
	 * @return The instance of the resource
	 */
	protected abstract Object initializeBean(BeanDefinition resourceDef);

	@SuppressWarnings("unchecked")
	@Override
	public <T> T getResource(String name, Class<T> type) {
		Object resource = this.getResource(name);

		if (type.isAssignableFrom(resource.getClass())) {
			return (T) resource;
		}

		return null;
	}

	@SuppressWarnings("unchecked")
	@Override
	public <T> T[] getResources(Class<T> type) {

		BeanDefinition[] matches = this.library
				.getMatches(new BeanLibrary.ResourceTypeMatcher(type));

		List<T> res = new ArrayList<T>(matches.length);

		for (int i = 0; i < matches.length; i++) {
			res.add((T) this.doGetResource(matches[i].getName()));
		}

		if (this.parent != null) {
			Object[] resources = this.parent.getResources(type);

			for (int j = 0; j < resources.length; j++) {
				res.add((T) resources[j]);
			}
		}

		Object arr = Array.newInstance(type, res.size());
		T[] arrt = (T[]) arr;
		return res.toArray(arrt);
	}

	@Override
	public <T> T getResource(Class<T> type) {
		BeanDefinition[] matches = this.library
				.getMatches(new BeanLibrary.ResourceTypeMatcher(type));

		if (matches.length != 1) {
			throw new BeanFactoryException(
					"No single bean with desired class found: " + type);
		}

		T[] resources = this.getResources(type);

		return resources[0];
	}

	@Override
	public void destroyScope(String scopeName) {
		log.info("Destroying scope named \"" + scopeName + '"');
		Scope scope = getScope(scopeName);

		State state = scope.getState();

		if (Ready == state || Idle == state) {

			BeanDefinition[] scopedResources = this.library
					.getResourcesOfScope(scope);

			for (BeanDefinition definition : scopedResources) {
				String name = definition.getName();
				Object resource = scope.getResource(name);

				if (resource != null)
					if (resource instanceof DisposableBean) {
						try {
							((DisposableBean) resource).destroy();
						} catch (Throwable t) {
							log.error("Error while destroing bean", t);
						}
					} else {
						String destroy = definition.destroyMethod();

						if (destroy != null) {
							// Call destroy method
							try {
								Reflect.on(resource).method(destroy).invoke();
							} catch (Exception e) {
								log.warn(
										"An error occurred while destroying resource: "
												+ name, e);
							}
						}
					}

				scope.removeResource(name);
			}

			scope.setState(Idle);
		} else {
			throw new ScopeDestructionException(
					"Scope is not in ready or indle state: " + state);
		}
	}

	@Override
	public boolean containsScope(String scope) {
		return this.registeredScopes.containsKey(scope);
	}

	@Override
	public boolean isScopeInitialized(String scopeName) {
		Scope scope = getScope(scopeName);

		if (scope == null)
			throw new ScopeInitializationException("Named scope not found: "
					+ scopeName);

		return Ready == scope.getState();
	}

	@Override
	protected void finalize() throws Throwable {
		log.info("Finalizing bean factory");
		for (Scope scope : this.registeredScopes.values()) {
			try {
				destroyScope(scope.getName());
			} catch (Exception e) {
				log.warn("Error while trying to destroy scope: " + scope);
			}
		}

	}
}
